Skip to content

spec: hold a predicate to what the engine can run; declare its fault semantics (ADR-0136) - #18985

Merged
os-zhuang merged 16 commits into
mainfrom
claude/issue-17778-field-rule-predicate-fault-semantics
Sep 20, 2026
Merged

os-zhuang merged 16 commits into
mainfrom
claude/issue-17778-field-rule-predicate-fault-semantics

Conversation

@os-elon-musk

@os-elon-musk os-elon-musk commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #17778

Clause-②: no

The domain:spec half of the maintainer ruling on objectui#8069 (decision batch #119 item 3, 2026-09-12: 「同意」 to A, with Q2 yes and Q3 yes), reworked under decision batch #160 item 1 on #19003 (letter A, maintainer 「同意」 2026-09-18T11:58Z). The renderer half is objectui#8069 and is not in this PR.

What this PR is now — a record, one producer fix, and nothing else

Ruling A removed the schema change from this PR. Decision batch #122 item 2 (card #15811, comment 5644350409, 2026-09-12) had already ruled the evaluated-slot narrowing across all 36 declaring positions — its own census names 「field / option / grid-column visibleWhen / readonlyWhen / requiredWhen」 — and PR #18638 owns it under one ADR-0087 id and lands first. Card #17778 ruled fault semantics, not the carrier symbol, so nothing ruled is lost.

Removed here (commit 9f30a18a9): the three FieldSchema triad-slot edits; the PredicateSchema / PredicateInputSchema rebinding, which go back to composing the persistence schemas, wide; the field-rule-predicate-evaluated-slot-source-required ADR-0087 entry (the entry file and, through gen:migration-registry, its registry rows) and the changeset marker; the triad pin test; the two ADR anchors; and every api-surface / reference-page row that existed only because of those.

Kept, per the same ruling: ADR-0137 (the fault-semantics record, renumbered — see below), the ADR-0089 pointer addendum, and the producer fix.

Re-derived and removed (see the measurement below): the ADR-0058 D7 roster entry.

The revert is byte-exact, not "close enough"

The three sources and the field.zod anchor were restored with git checkout d8b12fca97 -- (the merged-main parent of this branch's merge commit, a pinned sha, not a moving ref); git diff d8b12fca97 is empty for each. registry.ts was not hand-edited — the entry FILE was deleted and gen:migration-registry re-emitted the generated regions; the result is byte-identical to merged main and check:migration-registry is green.

After regenerating, the whole diff against merged main is 6 files, and the generated half of it is 4 lines:

artefact delta vs merged main
content/docs/references/** byte-identical — 0 rows
packages/spec/api-surface-declarations/ 4 lines, in root.txt and shared.txt, all of them the producer fix
packages/spec/api-surface/, export-origins/, declaration-map/ unchanged — check:api-surface green

The producer fix, on its own terms

cel() and expression() (hence F and P) always write a non-blank source, but were declared as returning Expression, whose source is optional — a declaration of a shape neither function can produce. The fix is at the PRODUCER (Prime Directive #12): the return type now states what the helper emits. The docblock was rewritten so it no longer rests on the triad requirement this rework removes; what it now says is the general fact plus the live consumer, FlowEdgeSchema.condition.

-declare function cel(strings: TemplateStringsArray, ...values: unknown[]): Expression;
+declare function cel(strings: TemplateStringsArray, ...values: unknown[]): EvaluatedExpression;
-declare function expression(source: string, dialect?: ExpressionDialect, meta?: ExpressionMeta): Expression;
+declare function expression(source: string, dialect?: ExpressionDialect, meta?: ExpressionMeta): EvaluatedExpression;

Narrowing a return type removes nothing from a caller — EvaluatedExpression is assignable to Expression — so no call site changes.

Clause-② re-judged, and the changeset level follows it

The previous body declared Clause-②: yes (narrowing). With the accept-set narrowing gone that declared something this diff no longer does, and Check Changeset read it and reddened. Re-judged against the six files that remain: Clause-②: no. The judgement is measured, not asserted:

question the declaration asks reading instrument
does it widen an accept set? no — no zod schema moves at all; packages/spec/src changes are two return types and two docblocks git diff d8b12fca97 -- packages/spec/src
does it narrow an accept set? no — same reading; the narrowing left with commit 9f30a18a9 as above
does it expand the public surface? no — no export added, removed or renamed check:api-surface green, with no removed-or-narrowed report

⇒ the changeset is patch, the BREAKING banner is gone and so is the ADR-0087 disposition marker — both belonged to the narrowing that owned them. Something published still moves (two return types in the shipped .d.ts), so skip-changeset would be wrong; a producer-side fix in a released package is exactly what a patch entry is for.

Re-run locally against a pull_request payload carrying this body, before pushing: GITHUB_EVENT_NAME=pull_request node scripts/check-changeset-no-major.mjs --base origin/main --event PAYLOAD_PATH → exit 0, NOT DECLARED — the clause-② declaration reads 'no'. The same command against the OLD body reproduces the CI red, so the local run is a measurement and not a hopeful one.

ADR-0136 is renumbered to ADR-0137

PR #18480 added docs/adr/0136-declared-journeys-as-priority-anchor.md about 42 hours earlier. scripts/check-adr-anchors.mjs prescribes exactly this — the NEW record takes the next free number, and renumbering an already-accepted record was ruled out, so before it is referenced is the only cheap moment.

0137 re-verified free on this rework, not inherited from the earlier sweep:

query result
git ls-tree origin/main docs/adr/ tops out at 0135 — neither 0136 nor 0137 on main
added docs/adr/* files across all 31 open PRs (GET /pulls/{n}/files) two hits, both 0136: #18480 and this PR. Zero on 0137

The scan lit twice on 0136, so the zero on 0137 is a reading and not a dead query.

Three corrections the record owed

1. Its Status line claimed an implementation it no longer has. It now says what is true: this record declares and implements nothing. D1's authoring refusal is batch #122 item 2's, carried by #18638; D2 / D3 / D4 are consumer-delivered in objectui#8069.

2. The gate-slot conversion is RULED and IN FLIGHT, not "filed as a follow-up". The dangling sentence is gone. The record's claim that converting the gate slots "would bake a direction the ruling did not give" is true of batch #119 and was silent about batch #122 item 2, which gave exactly that direction six days earlier, and about #18638 which implements it. The claim is now stated as what it is — a statement about which ruling authorizes what, not a reason the conversion should wait — and the lint-side cost (validate-visibility-predicates.ts's celRefusal records the opposite position today) is named as a cost #18638 carries, not as an objection.

3. The hand enumeration is replaced by a citation of #15811's census, because the hand list had already rotted. It omitted packages/spec/src/system/settings-manifest.zod.ts:424 and :686, both visible: SettingsVisibilityInputSchema, which is ExpressionInputSchema.superRefine(...). Measured through SettingsManifestSchema.safeParse on the built dist:

authored visible manifest slot (:686) specifier slot (:424)
{ dialect: 'cel', ast: … } ACCEPTED ACCEPTED
{ dialect: 'cel', source: ' ' } ACCEPTED ACCEPTED
bare ' ' ACCEPTED ACCEPTED
LIT CONTROL 'data.provider.toUpperCase()' REFUSED custom@visible REFUSED custom@specifiers.0.visible
DARK CONTROL "data.provider === 'smtp'" ACCEPTED —

The refinement is live at both slots and narrows neither the ast-only nor the blank-source arm — if (!source) return; is the line, and the lit control is what makes the six ACCEPTEDs a reading.

The ADR-0058 D7 roster entry — re-derived, then removed

The ruling's KEEP list names it, and carries NO re-derive clause. The instruction to re-derive came from this seat's dispatch brief, not from the maintainer — recorded here because the earlier wording attributed it to the ruling. The re-derivation concluded the entry no longer belongs (measured: discovery finds 37 positions with the line present and 37 without, floor 37 unchanged and met at 37; the alias types zero slots), so the line is removed here pending the maintainer's explicit confirmation of that removal.

The roster lists schemas that declare an expression surface — "a slot whose accepted grammar is narrower gets its own schema and must be listed here too". PredicateInputSchema earned its place only while the rebinding made it = EvaluatedExpressionInputSchema and bound the triad to it from another file. Reverted, it is a plain alias of ExpressionInputSchema typing no slot, and the ledger header's limitation 2 names it as the standing latent example — leaving it rostered would make that paragraph false. #18638 measured the same thing independently: 「PredicateInputSchema is a plain alias of ExpressionInputSchema with zero slot users; it stays wide with the schema it aliases」.

Measured twice on this branch with the revert already applied, by raising the head floor to 9999 through ablation-replace.mjs so the assertion prints the count:

roster entry PRESENT  -> discovery found 37 position(s) via 'head'   (floor 37)
roster entry ABSENT   -> discovery found 37 position(s) via 'head'   (floor 37)

Identical, because the three triad positions are head-matched by ExpressionInputSchema again. Both mutations landed and both restores verified on disk (anchor 1 -> 0, then blob == HEAD and git diff HEAD empty). Identity grep agrees: PredicateInputSchema has 2 hits under packages/spec/src/**/*.zod.ts — its own definition and its z.input companion, zero slots — against a lit control of 19 files for ExpressionInputSchema and a dark control of 0.

⛔ Not a gate weakening. The head floor stays 37 and is met at 37; no ledger row is deleted, no floor is lowered, no test is skipped or quarantined. The same three surfaces are discovered through the schema that types them. Both dogfood files are byte-identical to merged main. The same statement holds for the two other removals: the triad pin test and the ADR-0087 entry are removed because the behaviour they recorded is no longer in this PR — not to turn anything green. packages/qa/dogfood re-run after the removal: 7 passed (7).

⛔ GOVERNED SURFACE — this PR parks as a draft, by design

docs/adr/** is on the register, so this is the regime's correct resting state, not a stall. An authorized approval is owed before any seat lands this. This seat has not flipped it ready, has not enqueued it and has not armed auto-merge. The needs:contract-review carrier is the review seat's and stays hung; a fresh at-tier review is owed on this head.

Evidence

Acceptance notes

  • packages/spec/src/data/field.zod.ts:1513 expression (the formula slot) is an evaluated slot on ExpressionInputSchema. Untouched, and no longer this PR's business at all: it is inside feat(spec)!: every engine-evaluated expression slot requires a non-blank source #18638's 36-position census.
  • PredicateSchema / PredicateInputSchema have zero slot users — measured above. The ruling says they stay as they are and that a later card may retire them as dead symbols on their own measurement. No anchor is left claiming otherwise: the anchor this PR added for shared/expression.zod.ts is deleted, and the field.zod.ts anchor is back to its pre-PR text.
  • The ADR-0137 record's Consumers line still names shared/expression.zod.ts and data/field.zod.ts. That is the set of files the DECISION governs, which is unchanged; it is not a claim that this PR edits them for that reason.

维护者速读(草稿)

席位意见一节留空,由席位在 at-tier 评审后定稿为评论。

改了什么 — 按 batch #160 item 1 的裁决 A,把这个 PR 里的协议收窄整段拿掉:字段三条规则槽位(visibleWhen / readonlyWhen / requiredWhen)回到原样,Predicate* 两个别名回到原样(仍然是宽的持久化契约),对应的 ADR-0087 迁移条目、pin 测试、两个 ADR anchor、以及只因它们才产生的 api-surface 与参考文档行,全部删除。留下的是:ADR-0137(改号后的 fault 语义记录)、ADR-0089 的指针附录、cel / expression 的返回类型修复。另外按指示重新推导后,删掉了 ADR-0058 D7 的那一行 roster 条目。

为什么改 — 同一个收窄早在六天前就被 batch #122 item 2 裁决过了,覆盖全部 36 个求值槽位(包含本卡的三条字段规则),并且由 PR #18638 用一个 ADR-0087 id 承载、先落地。两个 PR 各带一份收窄,就是一次迁移两个 id、两份 CHANGELOG 说法。#17778 裁决的是 fault 语义,不是承载它的符号,所以记录留下、收窄交出去,没有任何被裁决过的东西丢失。

风险与代价(含回滚) — 风险很低:协议行为零变化(没有任何 zod schema 移动),对外只剩两个函数返回类型收窄,而 EvaluatedExpression 可赋值给 Expression,所有调用点照常编译。changeset 因此从 minor + BREAKING 降为 patch,Clause-② 重判为 no(三项读数在上表)。代价是本 PR 不再自带任何强制:D1 的编写期拒收要等 #18638;这一点在记录的 Status 和 Scope boundary 里明写了,不是留给读者去发现。回滚成本极低——本轮全部是删除与还原,恢复即 revert 这四个 commit。

席位意见 — (留空)

你要做的 — 这个 PR 碰了 docs/adr/**,属受管面,停在 draft 等一个授权批准,这是制度的正常终态。需要你看的是三件事:① ADR-0137 现在只声明不实现——D1 交给 #18638、D2/D3/D4 交给 objectui#8069,这个归属你是否认可;② 记录里原来那句「gate 槽位转换会写进裁决没给的方向」已改写为「那只对 batch #119 成立;batch #122 item 2 给了这个方向,#18638 正在做」,这个更正你是否同意;③ D7 roster 那一行被删而不是保留——测量是:删与不删,head 发现数都是 37(地板 37),且 PredicateInputSchema 零槽位使用,所以它不再是「更窄的别名」。裁决原文把它列在「保留」里,且未附任何重新推导的条款 —— 要求重新推导的是本席派发令,不是维护者(先前措辞把它归给了裁决,此处更正)。推导结论是该条目不再属于花名册(实测:该行在与不在,发现数都是 37,地板 37 未动且 met at 37;该别名零槽位),故此处删除,等您明确确认这一删除。


Generated by Claude Code

… fault semantics

`PredicateSchema` / `PredicateInputSchema` composed the PERSISTENCE contract
(`source` OR `ast`) while a predicate exists to be evaluated and the CEL engine
reads `source` alone. An `ast`-only envelope and a `source` blank after trimming
parsed, registered, passed `objectstack validate`, then faulted or
short-circuited at evaluation time. They now compose the evaluated rule, and the
field-rule triad on `FieldSchema` binds to them.

The predicate contract's docblock states the fault semantics the consumers are
held to: a faulting field-rule predicate refuses the SUBMIT naming the field and
the rule; visibility stays fail-open at RENDER; a blank or faulting gate
predicate is diagnosed, never a silent true. The evaluation helper's fallback
stays freely specifiable.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
The ADR-0087 D3 semantic entry for the D1 narrowing, the pin test for all
three field-rule slots, and the two ADR anchors. ADR-0089 gains a pointer:
it unified the `*When` family's NAME, this record decides what the family
does when it cannot run.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
…ed predicate

`gen:docs` was the one artifact `check:generated` proved stale; the published
reference now states `source` as required on `Predicate` / `PredicateInput`.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/shared/expression.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/shared/expression.zod.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json eeaa882459edffd077c1cda2972af06d1f10550b → packageMentionDocs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 18, 2026

os-elon-musk commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Type Check red on this head — diagnosed and handed to the implementer

Two checks red on head 4f9d0cf3c513b72d3393ea76c0429aa39c95bfb6, read 2026-09-18T0945Z, one root cause:

  • Type Check · consumer gates (job 105553471244)
  • Type Check · debt ledger (job 105553471301)

From the consumer-gates log:

examples/app-showcase typecheck: src/data/objects/invoice.object.ts(249,7): error TS2322
  Type '{ dialect: "template"|"cel"|"cron"; source?: string | undefined; … }'
  is not assignable to type
  'string | { dialect: "template"|"cel"|"cron"; source: string; … } | undefined'
      Types of property 'source' are incompatible.
        Type 'string | undefined' is not assignable to type 'string'.

Same error at lines 249, 279 and 285 of that one file.

⚠️ Correction by this comment's author, added after reading the second log. As first posted, this comment treated the two reds as one root cause and left the debt-ledger red as a question for the implementer. They are two distinct populations. Measured:

check-type-check-coverage --re-measure: 1 ledger entr(ies) drifted upward

  • @objectstack/spec-monorepo: DEBT records 26 raw tsc error(s),
    `tsc --noEmit` now reports 34 (+8).

That is the root package (@objectstack/spec-monorepo, its typecheck:root program), not examples/app-showcase. The three example errors belong to a separate workspace package with its own typecheck and do not account for the +8, so clearing the first red does not clear the second.

⛔ The remedy on the second red is bounded, and the gate says so itself: "DEBT is frozen debt, not a permission slip — the ledger is a ratchet and may only shrink. Fix the new errors — that is the author's remedy … ⛔ MAINTAINER-ONLY, NOT a co-equal option: … Raising the entry weakens a shrink-only ratchet … do not take this path to get CI green." So the eight new errors get fixed. Raising the DEBT entry, editing its numbers, or rewriting its note to accommodate them is not available to the implementer and not available to this seat either — weakening a gate threshold sits on this project's maintainer-only floor. If the eight prove genuinely irreducible inside this card's scope, the implementer stops and reports, and this seat escalates it as a decision rather than pressing the ratchet back.

Noted from the same run and ⛔ NOT this pull request's to fix: that ledger entry already carries a tier itemisation declared stale by compositionAt (tallied at 80, recorded 26), so its existing note does not describe what the pile is currently made of.

This is this pull request's to fix, and it is the narrowing working rather than a defect. The change makes source required in that slot; an in-repo example still writes it optional. Updating in-repo consumers that a landed narrowing breaks is part of landing it, so the three sites in examples/app-showcase are in scope — ⛔ not a reason to widen the pull request, and ⛔ not a flake (no re-run was spent on it).

One question was handed to the implementer with the diagnosis, and it is a question rather than a finding: PR #18952 landed earlier today publishing required-one-of(['source','ast']) on ExpressionSchema, so the contract there is source OR ast and an ast-only expression is legal. If this change requires source unconditionally, the ast arm would become unreachable — which the ruling did not ask for (its blank-predicate point is about a blank predicate, not about dropping ast). The expected type in the error is a union with string, which suggests a field-rule slot rather than ExpressionSchema itself, in which case there is no conflict at all. The implementer will measure which it is and state it; if the ast arm really did become unreachable, that exceeds the ruling and is the seat's to escalate rather than the implementer's to quietly keep or revert.

⚠️ Reminder on this pull request's resting state, unchanged by the above: it touches docs/adr/**, which the governed-surface predicate reports as governed, so it parks as a draft awaiting an authorized human approval. This seat cannot approve it, flip it ready, or enqueue it. A green head does not change that.


Generated by Claude Code

`expression()` and `cel()` (hence `F` and `P`) always set a non-blank
`source`, but were declared as returning `Expression`, whose `source` is
optional. Harmless while no slot required `source`; once the field-rule triad
did, a `P` template written straight into `visibleWhen` became a TS2322 — the
recommended authoring form for a predicate stopped type-checking in the one
place predicates are written (caught by `check:skill-examples` on the docs and
skills corpus, and by the examples typecheck on app-showcase).

Fixed at the producer, not the call sites: the return type now states what the
helper emits. Narrowing a return type removes nothing from a caller —
`EvaluatedExpression` is assignable to `Expression`.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>

os-elon-musk commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Dogfood Regression Gate red on the current head — the three slots this card is about left the conformance ledger

Live on head d3c10713d0883fbb997d7596f1fa20911d1cd826 (read 2026-09-18T1001Z), so this is a new failure and not the superseded batch. The earlier typecheck reds are fixed — 21 success on this head.

Dogfood Regression Gate (3/3), job 105556637504: 2 failed / 436 passed, both in packages/qa/dogfood/test/expression-conformance.test.ts, the ADR-0058 D7 expression surface conformance ledger.

STALE covers — surface no longer in source: data/field.zod.ts:FieldSchema.requiredWhen
STALE covers — surface no longer in source: data/field.zod.ts:FieldSchema.readonlyWhen
STALE covers — surface no longer in source: data/field.zod.ts:FieldSchema.visibleWhen

discovery found 34 position(s) via 'head' (floor 37) — a roster name immediately after
`field:` (the pre-#17630 scan). Discovery has lost a mechanism it is required to see.

37 − 34 = 3, and the three named surfaces are exactly the three field-rule predicate slots. The two assertions are one fact seen twice.

Why this is worth stopping on rather than clearing

visibleWhen / readonlyWhen / requiredWhen are the three slots this card exists to govern. The D7 ledger is the instrument that records which expression surfaces exist and whether anything evaluates them — the same class of instrument that made the unevaluated-predicate gaps visible in the first place. A change whose purpose is to make these predicates' fault semantics contractual should not end with them invisible to the contract ledger.

⛔ The remedy is bounded, and the gate says so itself

If a position was legitimately retired, lower the floor in SCAN_CONTROLS in the same commit that deletes its ledger row, and say which position went away; ⛔ do not lower it to make a re-narrowed scan pass.

So: ⛔ the SCAN_CONTROLS floor is not to be lowered, and ⛔ those ledger rows are not to be deleted to silence the STALE complaint. Lowering a discovery floor is gate-weakening, which sits on this project's maintainer-only floor independently of that message — not available to the implementer, and not available to this seat either. This is the same standing as the DEBT ratchet noted in the previous comment.

✅ RESOLVED, and the seat's hypothesis below was FALSIFIED — correction by this comment's author

Dogfood Regression Gate (3/3) is success on head e1978a0f3b02608d6137ac4daab928540bb52b6b (read 2026-09-18T1026Z; 28 success / 2 skipped / 0 failures). The implementer measured the cause and it is not what this comment hypothesised.

What actually happened. The three slots were NOT restructured out of the shape D7 scans for, and no expression surface was removed from the conformance ledger. They are still exactly name: SchemaIdentifier.optional(). Only the schema name changed — the slots now bind PredicateInputSchema — and EXPRESSION_INPUT_SCHEMAS is a fixed roster of names that did not contain that one. Alias resolution there is file-local, so the rename alone took the three positions out of discovery.

⭐ The ledger had already written this trap down. Its header carried it as limitation 2, naming this very alias as "latent rather than live … it types no slot anywhere". This card made the alias live from another file, which is precisely the condition that limitation described. The roster's own docblock prescribes the repair: "A new narrowed alias belongs in this list on the same commit that introduces it", with #7327 and #15807 as two prior instances.

The fix was therefore to ADD the roster entry — plus a correction to limitation 2, which this change makes false. ⛔ No floor was lowered. ⛔ No ledger row was deleted. The implementer reproduced CI byte-for-byte by removing that one roster entry again (same 3 STALE covers, same discovery found 34 position(s) via 'head' (floor 37), 2 failed / 5 passed), then restored it clean — so the entry is demonstrably the fix rather than a silencer.

⇒ The escalation this comment flagged as possible is withdrawn: nothing exceeded the ruling, and there is no maintainer decision owed on whether those three slots may leave the D7 ledger. They never left it in the sense that mattered; discovery simply could not see their new name.

The hypothesis is left below rather than deleted, because it was wrong in a way worth keeping: it was stated as a lead with an explicit request to falsify it, and falsifying it is what produced the actual fix.

What was handed to the implementer, and it is a lead rather than a finding

The hypothesis given: restructuring field.zod.ts moved those three declarations out of the shape D7 scans for, as an unintended side effect rather than a decision — in which case the fix is to restore a form discovery can see, ⛔ not to touch the scan or the floor.

The implementer was asked to falsify that if it is wrong, and told that if the three slots were deliberately restructured out of that surface, the correct action is to stop rather than proceed: removing three published expression surfaces from the conformance ledger is a contract-visible change the governing ruling did not ask for, which makes it an escalation to the maintainer rather than something to land or to wave through.

⚠️ This pull request's resting state is unchanged by any of the above: it touches docs/adr/**, so it parks as a draft awaiting an authorized human approval that this seat cannot give. A green head does not change that.


Generated by Claude Code

…/main

The declaration-text pins main brought in #18971 move with this branch's two
facts: `source` becomes required wherever the predicate contract composes, and
`cel` / `expression` now declare the `EvaluatedExpression` they always emitted.
Exactly 8 distinct changed lines across all six files; the line count is those
two facts repeated at every composing site.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>
… schema

ADR-0136 D1 binds the `FieldSchema` field-rule triad to `PredicateInputSchema`
from another file, and alias resolution in the D7 discovery scan is FILE-LOCAL
— so all three surfaces dropped out of discovery and their covers went STALE,
34 positions found against a `head` floor of 37.

The ledger header had written this trap down as limitation 2, naming this exact
alias as latent; this change makes it live, so the roster gains it and the
limitation is corrected to say so. ⛔ No floor was lowered and no ledger row was
deleted: the three surfaces still exist and are still exactly what the scan
looks for. The roster docblock already prescribed this repair for a new alias.

Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

PM note — D4's carrier is filed, two numbers in the record are off, and the PR body is stale. None of this is a verdict; the at-tier clause-② review is still running.

1. The follow-up ADR-0136 promises now exists: #19000.

The Scope boundary section says of the gate-slot conversion: "That conversion is filed as a follow-up with the slot inventory, and it is the one place where D4 is currently declared ahead of its authoring-side enforcement." #19000 is that follow-up and carries the inventory. It is finding only — no domain:*, no priority, those are triage's to set.

The sentence in the ADR does not cite a number, so the reference is still dangling in the text. Adding #19000 to it is a one-line edit I am deliberately not requesting right now: the review is mid-flight against e1978a0f3b02608d6137ac4daab928540bb52b6b, and re-heading the PR under a running review buys a dangling-reference fix at the cost of the review's subject. I will decide it at the landing step.

2. Two numbers in the Scope boundary are off, both in the direction that strengthens its own argument.

Instrument: git grep for slot definitions composing ExpressionInputSchema directly or via the derived SettingsVisibilityInputSchema / ActionConditionInputSchema in packages/spec/src, excluding *.test.ts, then a 15-line window per slot for a declared fault direction. Read on origin/main at f347c793e16322a4befc77651d1ab8760bf36874 and on this PR's head, taken 2026-09-18T10:36Z. Every window had a lit control; zero dark instruments.

  • The record says "several of those slots carry their own declared fault directions". Measured: exactly 4 of 21, and they disagree 3-to-1 — fail-closed at data/object.zod.ts:1194, data/object.zod.ts:1387, ui/bulk-action.zod.ts:209; fail-soft at data/object.zod.ts:1390 (disabledWhen). The other 17 declare no direction at all. A 3-to-1 split with 17 abstentions is a harder case against converting them in one sweep than "several" conveys, so this corroborates the deferral rather than undercutting it.
  • The Scope boundary's enumeration names 19 slots. The same instrument finds 21. The two unnamed are packages/spec/src/system/settings-manifest.zod.ts:424 and :686, both visible: SettingsVisibilityInputSchema, which is ExpressionInputSchema.superRefine(...): the refinement adds a check but does not narrow the ast-only or blank-source arm, so their exposure is identical to the named 19. Control for that zero: the record mentions settings-manifest or SettingsVisibility 0 times against 10 hits for visibleWhen in the same file.

Both readings went to the reviewer as evidence to check, explicitly not as a verdict and explicitly not as grounds to move its PASS or FAIL.

3. The PR body was written once and now predates three developments.

It has not been rewritten since it was opened, and three things landed on the branch after it: the DEBT producer fix, the D7 roster entry in EXPRESSION_INPUT_SCHEMAS, and the ast-arm type-level measurement answering whether #18952's required-one-of(['source','ast']) survived. A reader taking the body as current will be reading three developments behind the head. Recording it here rather than editing the body, for the same reason as item 1.

4. Landing posture, restated so it is on the record and not only in a seat post.

This PR touches docs/adr/**, which is a governed surface. Whatever the review returns, the landing path stops short of the queue: on PASS I strip both needs:contract-review carriers, post the ## Contract review record in the template shape, confirm check-clause2-carriers.mjs --pair 18985 exits 0, and stop there. I do not flip it ready, do not attach auto-merge, and do not enqueue. It parks in draft awaiting a human approval that this seat has no authority to give. Landing does not happen on this shift.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Clause-② review returned — PASS with findings, and this PR is HELD. Carriers stay on.

Read this as the seat's disposition, not as the record of record. No ## Contract review record is being written on this head, and neither needs:contract-review carrier is being stripped. Reasons below.

Review admissibility. Isolated at-tier review, dispatched because this seat serves below CONTRACT_REVIEW_TIER (scripts/pm/dispatch-gates.mjs:11899). Tier verified the only way it can be — by census of the reviewer's own transcript, since a subagent's get_session reads the parent and cannot self-attest: 88 of 88 assistant turns at the tier constant's value, zero at any other model. Bound to head e1978a0f3b02608d6137ac4daab928540bb52b6b, stated in full by the reviewer for every reading.

Verdict: CLAUSE-② REVIEW: PASS. Accept-set narrowing on the FieldSchema triad and the public PredicateSchema / PredicateInputSchema / Predicate / PredicateInput; persistence contract measured un-narrowed; minor + BREAKING + ADR-0087 D3 entry present and consistent; major correctly not used. All ten review items PASS. GATE-WEAKENING CHECK: none taken — SCAN_CONTROLS floors unchanged at 37/3/2, D7 ledger 26 rows to 26 rows with non-comment bytes identical, scripts/check-type-check-coverage.mjs byte-identical with spec-monorepo errors: 26 intact, zero deleted files, zero added .skip/.only/.todo/xit/xdescribe lines against a lit control of 9 added it(' lines, zero removed lines in any test file.

Why it is held anyway — one blocking finding, and it is not a clause-② matter.

The reviewer found, and I have independently confirmed against primary sources, that this record is silent about a standing ruling that covers part of its own subject:

So the record's Scope-boundary rationale — that converting the gate slots "would bake a direction the ruling did not give" — is true of batch #119 and silent about batch #122, which gave exactly that direction for those slots. A governed record must not reach its approver with an incomplete account of the standing rulings on its own subject. That is the hold.

Also established, separately from the review. This record's ADR number collides: docs/adr/0136-declared-journeys-as-priority-anchor.md is added by open PR #18480, created 2026-09-16T15:07:00Z, about 42 hours before this PR. I swept all 32 open PRs for added docs/adr/NNNN- files — exactly two, both claiming 0136, zero parse errors — and 0137 is free on origin/main at f347c793e16322a4befc77651d1ab8760bf36874 (highest record there is 0135) and unclaimed by any open PR. The fix is the one scripts/check-adr-anchors.mjs:545 prescribes: the new record takes the next free number. Renumbering an already-accepted record was ruled out in #5992, and neither of these two is accepted yet, so that ruling does not arbitrate between them — taking 0137 is simply the option that needs no other lane's cooperation.

Why no record and no carrier strip on this head. The gate reads the PR's live head (check-clause2-carriers.mjs:5407), and a head that moves after the carriers are cleared earns a re-hang row (:7082, :7249). The head must move — for the reconciliation edit, the renumber and the migration-entry wording fix. A record written against e1978a0f would be invalidated by the very next push. So the carriers stay on, which is the honest state: 「开着的载体恒 = 真实待审」. The reviewer has confirmed it is ready to re-affirm against the renumbered head with the rename diff only.

Not mine to decide, and going to the maintainer. Which binding the record should describe, whether this PR's ADR-0087 entry folds into or cross-references evaluated-expression-slots-source-required, and which of two seats' PRs lands first on twelve shared files, are cross-seat and architectural questions. #18638 belongs to another seat in this lane; I hold #17778 and this PR. I am not answering them for the maintainer and I am not touching #15811 or #18638 beyond posting evidence on #15811 (comment 5728924245, evidence only, no label, assignee or state change).

Disposition of the follow-up card I filed earlier today. #19000 is closed as a duplicate of #15811. It re-opened a question batch #122 had already settled. My error, recorded on both cards.

Non-blocking, carried for whoever edits the record: the migration entry's surface wording "the public exports every other declaration site composes" is false — zero declaration sites composed them, measured; tmpl() / cron() still declare Expression while always setting source, which #18638 already narrows; the PR body now predates four developments, the #18638 intersection included.

Seat: domain:spec#3. Posted 2026-09-18T10:50Z.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 209/209 CONTRACT_REVIEW_TIER
Head-sha: 1bbe8f56243a8b2644d364198b6fdae9f5647cd2

① Derived judgments

Isolated at-tier re-review. The earlier PASS on this PR judged e1978a0f3b02608d6137ac4daab928540bb52b6b at 22 files and is void for this head — decision batch #160 item 1 (letter A) reduced the diff to 6 files, so that verdict was not inherited and the reviewer confirms it did not consult it. Tier verified by census of the reviewer's own transcript at 209 of 209 assistant turns; a subagent cannot self-attest, its get_session reads the parent.

Ruling A's REMOVE list is measured removed, byte-exactly: 16 non-diff paths of the former 22-file set carry blob SHAs identical to the pinned parent d8b12fca97, and the restricted git diff parent..head over them is empty. The inverse was checked too, because the ruling forbids publishing the aliases narrowed: PredicateSchema = ExpressionSchema and PredicateInputSchema = ExpressionInputSchema read identical at head and at origin/main, and their rows in the two api-surface shards are main's wide union verbatim. The KEEP list survives: ADR-0137, the ADR-0089 addendum, and the producer fix present in source and in the built .d.ts.

The renumber holds on an independently widened instrument: origin/main tops at 0135, the head carries 0137 with zero 0136 hits anywhere in the tree, and a collision scan across all 34 open PRs (three more than the dev's 31, opened after 13:00Z) finds exactly two added ADR records — #18480 at 0136 and this PR at 0137. The lit pair fired.

② Semver level

Clause-②: no with @objectstack/spec patch, no BREAKING banner, no ADR-0087 marker — confirmed as the right set rather than the permitted one. Zero zod bytes move. The published movement is real and was not waved away: two return types reach the shipped .d.ts, proven by a lit control (swapping the parent shard back makes the declarations gate report ~ cel, ~ expression, "0 removed, 0 added, 2 reshaped", exit 1). A tsc probe in both directions shows a caller loses nothing while an implementer of typeof cel would now break — and no such implementer exists in this repo. ⚠️ Recorded because it matters to a reader: check:api-surface is names-only and blind to return types, so its green is not evidence of no .d.ts change; the declarations gate is the instrument that sees this.

③ Boundary flags

Gate-weakening: none. SCAN_CONTROLS reads 37 / 3 / 2 unchanged, the dogfood test and ledger blobs are byte-identical to the parent, and no floor, ratchet, baseline or ledger file appears in the 6-file diff. This PR deletes three things and each was adjudicated separately rather than in aggregate: the pin test (pinned refusals of a narrowing no longer shipped), the ADR-0087 entry (recorded a migration no longer performed; registry.ts re-emitted and blob-identical to parent), and the ADR-0058 D7 roster line (inert at this head — discovery finds 37 positions with it present and 37 without, against a lit control of 8 when ExpressionInputSchema is removed from the roster, so the scan is provably live and 37 is met at 37, not lowered to fit). All three record behaviour this PR no longer has. No test skipped, disabled or quarantined.

Implemented-by: claude/issue-17778-field-rule-predicate-fault-semantics
Reviewed-by: session_019srGWGCBBCBHqcDoRZpQRh

VERDICT: PASS

⚠️ PASS is not clearance to land, and one blocking item is owed to the maintainer. The reviewer's B1: the ruling's KEEP list names the D7 roster entry and carries no re-derive clause — the re-derive instruction came from this seat's dispatch brief. In substance the removal is a consequence of ruling A (the entry's only premise was on the ruling's own REMOVE list), but in procedure it is a departure, and this PR's body had attributed the instruction to the maintainer. That wording is corrected as of this act, in both languages, and the removal now goes to the maintainer for an explicit one-line confirmation with the alternative stated. ⛔ This seat does not reverse a ruling on its own reading.

Landing is additionally gated twice over: docs/adr/** is a governed surface and this seat has no authority to approve it, and ruling A sequences this PR to rebase after PR #18638 lands.

Non-blocking, carried: ADR-0137's Consumers header is acceptable but is the one sentence a reader can take for a claim about this diff rather than the decision's scope; after #19024's revert lands the two shard hunks drop on rebase and this PR's "4 lines in api-surface-declarations/" sentence needs rewording, since the patch evidence becomes a build plus a .d.ts grep; the dev's "every error TS2307" over-claims (measured 257 TS2307 plus downstream codes, conclusion unchanged); and whether objectui or cloud type anything against typeof cel — the only shape this narrowing can break, and the only path letter C's consumer-compile gate would see it through — is not measured at the pinned sha.


Generated by Claude Code

…eld-rule-predicate-fault-semantics

# Conflicts:
#	packages/spec/src/shared/expression.zod.ts
…/main

Discharges the os-regen deferral from the prior merge commit. root.txt and
shared.txt now reflect main's #18638 evaluated-expression-slot narrowing
(source becomes required across the composing slots) plus the drift picked
up while catching this branch up to main's current tip. No hand edits.
Main shipped the identical /`expression` return-type narrowing to
EvaluatedExpression first, under #18638 (card #15811) -- confirmed by the
merge: both sides made the same change independently, and ADR-0137's own
status line says its PR carries no schema change. Re-announcing that
narrowing here would duplicate #18638's own changeset entry in the same
release. Drop the redundant paragraphs and keep only what #17778 alone
ships: the ADR-0137 predicate fault-semantics contract and its ADR-0089
addendum.
@os-zhuang
os-zhuang marked this pull request as ready for review September 20, 2026 12:55
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 20, 2026
Merged via the queue into main with commit 092d460 Sep 20, 2026
37 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-17778-field-rule-predicate-fault-semantics branch September 20, 2026 13:43
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ion's two halves one call (objectstack-ai#19005)

Part of objectstack-ai#18670 — item 2, the **third** of the ruling's four named arms.
objectstack-ai#18670 remains open: banned keys is still untaken, and this body
deliberately carries no closing keyword for that number.

Clause-②: yes (narrowing)

Director ruling batch objectstack-ai#154 item 3, letter **C** (comment 5725370614,
maintainer 「同意」): 「the projection emits a refinement only where the rule
is a complete, mechanically derivable JSON Schema pattern — banned keys,
required-one-of, non-blank — one ledger row at a time; everything else
stays annotated as `x-dropped-refinements`」.

Continues PR objectstack-ai#18952 (squash `5e5ec9fa42194723cc523a274e7221c8447c4487`),
which landed `required-one-of` and `non-blank-string`.

## 1. The arm: `dependentRequired`

`data/SSLConfig`'s refinement is `hasCert === hasKey` — precisely
`dependentRequired { cert: ['key'], key: ['cert'] }`. It is emitted
through the same closed-vocabulary mechanism the previous arm built:
`src/shared/refinement-projection.ts` declares,
`scripts/lib/refinement-projection.ts` emits. No second mechanism was
introduced.

**Exact, not approximate.** A key absent from a JSON object is the only
way for its value to read `undefined`, and `dependentRequired` triggers
on PRESENCE — so a key present with any JSON value, `null` included,
arms its dependency exactly as the predicate's `!== undefined` does. The
dependency map is read once into the declaration and the predicate reads
it from there, so the published keyword and the enforced rule cannot
name different keys.

### Ledger: the rows retired, by name

`packages/spec/dropped-refinements.baseline.json`, **201 entries / 553
sites → 200 / 551**:

| row | before | after |
|:---|:---|:---|
| `data/SSLConfig` | `sites: [""]` | **deleted** — drops nothing now |
| `data/SQLDriverConfig` | `sites: ["", "sslConfig"]` | `sites: [""]` —
the `sslConfig` site closed |

1 row deleted, 1 row shrunk, **2 sites closed, 0 sites added anywhere**;
the ledger diff is deletions only. Generator census after: 551 dropped
across 200 published schemas, **199 projected** — 137 `required-one-of`,
60 `non-blank-string`, **2 `dependent-required`** — 3 undecidable.

`data/SQLDriverConfig`'s remaining `""` site is its **own** separate
rule, "`sslConfig` is required when `ssl` is **true**". That judges a
VALUE, is `if`/`then` rather than this arm, and correctly stays dropped
and annotated.

### Banned keys (`propertyNames` / `not`) — NOT taken, and not forced

Confirmed against the tree, not assumed: the nearest sites judge a
banned VALUE on a string (`FILTER_ARRAY_LOGIC_KEYWORDS`) or an allowed
key set that is data-dependent (`ai.paramHints` against the action's own
params). Neither is mechanically derivable, so **no candidate was
constructed**. This is why the body says `Part of` and carries no
closing keyword.

## 2. Mechanism fix A — the verdict is per NODE, the rules are per CHECK

`verdictFor` compared a node with ALL custom checks against the node
with NONE, so any one declared arm marked the whole node `projected`.
Reproduced on the landed code before changing it:

```
mixed(declared+undeclared)  dropped: []   projected: [{count: 2, declaredPatterns: ['non-blank-string']}]
undeclared-alone  (lit)     dropped: [{count: 1, declaredPatterns: []}]   projected: []
declared-alone    (lit)     dropped: []   projected: [{count: 1, declaredPatterns: ['non-blank-string']}]
```

A second refinement on a declared node was therefore neither ledgered
nor annotated, and the generator's UNDECLARED line could not see it —
silently violating the ruling's own 「A refinement that is not one of
these named patterns stays dropped and annotated」.

**Fix:** `projected` now requires `customs.length ===
declaredPatterns.length`; anything else is `dropped` conservatively. The
RAW differential is kept as a new `projectionMoved` field so the
detector still MEASURES rather than asserts — collapsing it would have
made the instrument blind to the zod upgrade it exists to notice — and
the generator prints partially-stated sites on their own line.

**Ablation, both directions** (anchor-verified on disk,
`scripts/ablation-replace.mjs`):

| leg | blob | result |
|:---|:---|:---|
| mutated — drop the `total === stated` guard | `54ed82dbe4c2` to
`2c1bff777363` | **1 test red**, 42 green: "a DECLARED arm beside an
UNDECLARED rule stays `dropped`" |
| restored | back to `54ed82dbe4c2`, `git diff HEAD` empty | **43 / 43
green**; mutant text on disk 0, guard text 1 |

## 3. Mechanism fix B — generator/detector coupling, by construction

`build-schemas.ts` (three `toJSONSchema` calls) and `projectOrNull` each
passed the `override` independently. **Measured on the pristine base**
with only the generator's import stubbed out:

| leg | gate exit | `shared/Expression.json` `allOf` |
`x-dropped-refinements` | files carrying the non-blank pattern |
|:---|:---|:---|:---|:---|
| base, untouched (dark control) | 0 | present | absent | **35** |
| base, generator-side override dropped | **0 — GREEN** | **absent
(wide)** | **absent (SILENT)** | **0** |

Census identical to an untouched run (553 / 201 / 197). That is the
item-1 silence restored, standing behind a green ratchet — worse than
the state the card was filed about, because the ledger now certifies it.
A merge-conflict resolution was enough to cause it.

**Chosen fix: one shared projection helper** —
`projectPublishedJsonSchema` in `scripts/lib/refinement-projection.ts`.
All three generator calls, the union-branch projector behind the third,
and the detector's differential now reach `z.toJSONSchema` through it,
and `projectByPruningUnionBranches` no longer takes an `override` option
at all. There is no argument left for a caller to forget.

**Why the sandbox-builder pin was rejected**, not overlooked: a pin
*detects* after the fact and can be skipped, deleted or made vacuous,
and it leaves the two-argument shape in place so the next merge conflict
can still separate them. The choke point makes the one-sided failure
**unrepresentable** rather than caught. Both halves now lose the
override together or not at all — which is what turns the ablation from
silent into loud. The test file's own `publish()` helper was rewired
through the same call for the same reason, so the unit pins measure the
real seam rather than a re-spelling of it.

**Ablation, both directions:**

| leg | gate exit | `Expression.json` `allOf` | `x-dropped-refinements`
|
|:---|:---|:---|:---|
| mutated — override removed from the ONE helper | **1 — RED**: 46
undeclared schemas + 76 miscounted ledger entries | absent (wide) |
**present (annotated)** |
| restored | 0 | present | absent |

The contrast is the whole point: before, one-sided removal was green and
silent; now it is red **and** the file confesses.

## 4. Contract: the published file narrows toward what the runtime
already refuses

**Whole published tree, base vs head:** 1530 of 1532 files
byte-identical. The two that move are `data/SSLConfig.json` and
`data/SQLDriverConfig.json`, each gaining `dependentRequired` and losing
the matching `x-dropped-refinements` row. Nothing else in
`packages/spec/json-schema/**` changed.

**Parse-equivalence probe — 10,368 documents** (2,592 SSLConfig-shaped
over the full presence lattice of 4 keys times 6 value shapes including
`null`, a wrong type and an unrecognised extra key; 7,776
SQLDriverConfig documents embedding each of those under three `ssl`
states). Published-side verdicts computed with ajv 8.20.0 (draft
2020-12) against the two real snapshots.

| reading | SSLConfig | SQLDriverConfig |
|:---|--:|--:|
| documents | 2,592 | 7,776 |
| runtime accepts | 60 | 180 |
| published accepts, base | 27 | 54 |
| published accepts, head | 15 | 30 |
| **narrowed by this arm** | **12** | **24** |
| widened | 0 | 0 |
| **documents the runtime ACCEPTS that the published file now refuses**
| **0** | **0** |

**Runtime behaviour did not move.** The runtime verdict vector is
byte-identical at merge base and head over all 10,368 documents — sha
`9e7c848f04e0c687` (SSL) and `4f18f835d4d1a62e` (SQL) on both sides. The
base leg was run against the real base blobs (`git checkout` of the two
source files at `d8b12fca9`, blob hashes asserted both ways, restore
proven by an empty `git diff HEAD`), not against a retyped predicate.

**LIT CONTROL for that zero** — weakening the dependency map to one
direction (`{ cert: ['key'] }`) moves **96 documents** (24 SSL + 72 SQL)
and lifts runtime accepts from 60 to 84 and 180 to 252. The zero is a
reading, not a silence.

Note the published-accepts figures sit below runtime-accepts on both
sides: `SSLConfig.json` is the OUTPUT shape and lists
`rejectUnauthorized` as required because the runtime applies its
`.default(true)`. That asymmetry is pre-existing, is the `x-io`
convention, and is unchanged by this PR — it is reported rather than
netted out.

## 5. Verification

- **Gates:** derived from the merge base with `node
scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands`, re-derived after the `origin/main` merge (identical, **84
commands**). Every exit code captured by redirecting to a file first,
never through a pipe. **80 exit 0, 0 findings.** The remaining 4 —
`check:doc-formula-expressions`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure`, `check:type-check-debt` — exit **3**, which
those gates define as `PREREQUISITE NOT MET` ("Nothing was measured ...
It is NOT a finding"): each reads BUILT output of packages outside this
diff. They are **NOT MEASURED**, not red; the re-run against a full
build is reported on the card.
- The derivation's own caveats are carried, not netted out: 50
artifact-roster families score `silent` for every card in the tree, 11
declare a population too wide to place, 5 take a value from the
workflow, and 5 path-scheduled CI jobs run 30 steps with no local
invocation. None of those is a clearance, and CI owns them.
- **`pnpm --filter @objectstack/spec check:generated`:** all 16
generated artifacts up to date. **`content/docs/references/**` does not
move** — see acceptance notes.
- **Targeted tests:** `scripts/refinement-projection.test.ts`,
`scripts/dropped-refinements.test.ts`,
`scripts/union-branch-projection.test.ts` — **91 / 91**. `packages/spec`
typechecks clean (`tsc --noEmit` over both the package and
`tsconfig.scripts.json`). The full `@objectstack/spec` suite reading is
on the card.
- **Lint, declared narrowing:** eslint run over the 9 changed lintable
files, 0 errors / 0 warnings, file count read from `--format json`. The
population is `eslint.config.mjs`'s own `files:
['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`; the config states in its own
words that this repo "never enables type-aware linting (no
`parserOptions.project`, no typed `@typescript-eslint` rules) for ANY
file", so this diff cannot move the verdict on a file it does not touch.
The repo-wide sweep is CI's run.
- `origin/main` merged through `bash scripts/pm/os-regen-merge.sh` (no
rebase, no force-push). It brought one docs-only commit, objectstack-ai#18979,
overlapping none of this branch's paths and no `merge=os-regen` path.
The previous arm's implementation body was asserted still present by
quoted-exact-name `git grep` against `origin/main`, with a dark control
at 0.

## Acceptance notes

Noted, not filed — out of scope for this card and not one of the three
filable classes:

- `packages/spec/scripts/build-schemas.ts` (the authorable-surface
docblock, near line 846) still names the retired
`api-surface-signatures.json`. The previous seat handed this to "the
next editor of `build-schemas.ts`", which is this PR. It is left
untouched deliberately: it is a stale code comment, not a defect, a
contract violation or an authoring trap, and the bounded in-place
exemption requires the finding to be **the same defect class as this
card**, which it is not. Carrier: the next PR that edits that docblock
for its own reasons.
- The dispatch's overlap warning — that a new keyword might move
`content/docs/references/**`, four pages of which open PR objectstack-ai#18985 edits —
**measured FALSE**. `dependentRequired` is a sibling keyword the
reference renderer does not read, `check:docs` is green and
`check:generated` reports all 16 artifacts current. No reference page
moves, so there is no collision with objectstack-ai#18985 on that directory.

Reported for the seat to file (a candidate class-(b) finding,
deliberately NOT fixed here):

- `packages/spec` ships `src/**/*.zod.ts` in `files[]`, and
`scripts/check-published-files.mjs` allows it with the reason "The Zod
schemas are themselves the contract (Prime Directive objectstack-ai#1); **downstream
code imports them directly**, so these sources are product rather than
build input." Two measurements contradict that reason: (1) the package's
`exports` map exposes no `./src/*` subpath and no wildcard, so no
consumer can import those files at all; (2) 188 of the 202 shipped
`*.zod.ts` files carry a relative import resolving to one of 35 modules
under `src/` that the glob does NOT ship (`src/shared/lazy-schema.ts`
alone is imported by 181 of them), so they would not resolve even if
reachable. Overwhelmingly pre-existing and far outside this card; this
PR adds the third importer of one of those 35. Not verified by `npm
pack` and not by a real consumer import — that is the next step for
whoever takes it.

---
_Generated by [Claude
Code](https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…site trace-sampling condition (objectstack-ai#19084)

Fixes objectstack-ai#18118

Clause-②: yes — retiring a published authorable surface. Carrier: the
changeset
`.changeset/18118-retire-observability-cel-arms.md`, which declares the
same line and the
ADR-0087 disposition. ⛔ The `needs:contract-review` label is the seat's
act; this PR neither
hangs nor clears it.

Ruling: batch objectstack-ai#160 item 3, **letter A**, maintainer 「同意」
2026-09-18T11:59Z — retire the two CEL
expression arms under ADR-0049 enforce-or-remove, by the
`spec-property-retirement` playbook.

## What was removed

Two union arms, not two keys:

| slot | was | is |
| --- | --- | --- |
| `ServiceLevelIndicatorSchema.successCriteria` | `z.union([{ threshold,
operator, percentile? }, EvaluatedExpressionInputSchema])` | the
structured object alone |
| `TraceSamplingConfigSchema.composite[].condition` | `z.union([
StructuredFilterRecord, EvaluatedExpressionInputSchema ])` | the
structured filter record alone |

`EvaluatedExpressionInputSchema` itself is untouched —
`packages/spec/src/shared/expression.zod.ts`
is not in this diff at all (it is held by PR objectstack-ai#18985, which is not
addressed here). What left is two
references to it.

## The card's zero, re-derived — and the instrument's radius

Re-derived on this branch's base
`176b03582e600ee5628d21bff9422073c5a5530c`, not inherited.

`git grep -n` over the whole tracked tree for `successCriteria`,
`ServiceLevelIndicator` and
`TraceSamplingConfig`. Every hit outside `packages/spec/src` is a
generated artefact
(`api-surface*`, `authorable-surface*`, `authorable-defaults`,
`declaration-map`,
`export-origins`, `json-schema.manifest`,
`dropped-refinements.baseline.json`), a reference page,
a changelog or changeset, or the shipped skill's prose row. Inside
`packages/spec/src` the readers
are: the two schemas' own unit tests,
`shared/evaluated-slot-population.test.ts` (a census), the
migration registry's prose, and one docblock in
`shared/evaluated-slot-union.ts`. Outside the spec
package the only reader is
`packages/qa/dogfood/test/expression-conformance.ledger.ts` — a
classification ledger, not an evaluator. **No service, plugin, runtime
or CLI path reads either key.**

**Reachable radius of the instrument:** tracked files in THIS checkout
at THIS commit. It does not
reach untracked or ignored build output, another repository, or a
published npm tarball.

**One known target outside it:** the sibling repository
`objectstack-ai/objectui`, which is on this
box but is a different git repository, so no `git grep` here can see it.
It is named rather than
waved at, because the `template-title-format` row of the same ledger
records exactly this limit for
a different key: an interpolation site that lives there and cannot be
measured from here. ⛔ **The argument that used to stand here was
REJECTED by at-tier contract review and is withdrawn.**
It claimed the radius was closed by a positive fact — that nothing in a
sibling could evaluate these
slots without importing the symbols naming them, whose consumers
`export-origins/` and the
`Console Pin Gate` enumerate. That is wrong on three counts the review
measured: `export-origins/`
records by its own description which SOURCE DECLARATION each exported
name resolves to — origins,
NOT consumers; the `Console Pin Gate` is path-filtered and was SKIPPED
on this very PR; and an
evaluator need not import either symbol, since a REST-served metrics
config can be read by key.

**What closes the radius instead is direct measurement outside it, with
a lit control in each repo.**
objectui @ `3e4f6324f7`: `successCriteria` 0 files,
`ServiceLevelIndicator` 0, `TraceSamplingConfig` 0;
controls in the same run — `visibleWhen` 340 files, `ObjectSchema` 652.
hotcrm @ `087b7c5dc4`
(887 tracked files; public, served by this session's git proxy — an
earlier dispatch's 「unreachable」
was the seat's error): the same three at 0 plus `slis` 0; controls —
`visibleWhen` 15, `defineStack` 47,
`@objectstack/spec` 269. `sampling` shows 4 files there and **every one
was read** (an MCP capability
table row, two prose sentences, a CHANGELOG line — no trace-sampling
config), so that zero stands on
inspection and not on the count.

**Known targets still OUTSIDE the radius, named rather than waved at:**
`objectstack-ai/cloud`
(access denied to this session) and any third-party npm consumer of
`@objectstack/spec`. Neither was
measured, by anyone. What BOUNDS the cost of a wrong zero there is the
retirement's **audibility** (a bound, ⛔ not a closure — at-tier review's
wording): a surviving predicate is a
`tsc` error or a parse refusal carrying the prescription, never a silent
change.

**Every symbol was located by its declaration site**, and a literal
inside a `//` or `/** */` comment
was counted as prose, not as a reader — that is why
`shared/evaluated-slot-union.ts` is listed as a
docblock and `migrations/registry.ts` as prose. Exit codes were captured
before any pipe.

## The retirement kit

- **The prescription hangs on the surviving schema's own `error` map**,
dispatched on `issue.input`
(the `HookBodyCapability` / `object.managedBy: 'system'` pattern).
`retiredKey()` and an ADR-0087
D2 strip both retire a KEY; neither retires an ARM, and the keys survive
here.
- **Where the prescription reaches, measured on zod 4.4 and pinned both
ways.** A schema's `error`
map is consulted for the top-level `invalid_type` a NON-OBJECT raises
and not for the child issues
a wrong-shaped OBJECT raises. So on `successCriteria` the bare-string
spelling carries the
prescription and the `{ dialect, source }` envelope is refused by the
structured arm's own
missing-key issues; on `condition` both spellings carry it, because the
record arm's aborting
`dialect` refine sees the object itself. The negative is pinned too: a
value refused for a reason
  that is NOT the retirement must not borrow its sentence.
- **ADR-0087 disposition: a D3 SEMANTIC entry**,
`observability-cel-predicates-retired`

(`packages/spec/src/migrations/entries/semantic/18.observability-cel-predicates-retired.ts`,
with
`registry.ts` **regenerated**, never hand-edited between the markers). A
predicate is an intent no
threshold/operator pair or attribute filter records. Both prescriptions
therefore carry **no**
`os migrate meta` sentence — owed only where a conversion covers the
surface.
- **Changeset** with the FROM → TO table and the one-line fix,
`@objectstack/spec` minor with the
  BREAKING banner, per the ruling's Execution section.
- **Baselines and reference pages regenerated**, never hand-edited:
`api-surface-declarations/`,
`authorable-defaults/`, the two `content/docs/references/system/*.mdx`
pages.

## Acceptance notes

- **FOUR published JSON Schemas change projection direction,
mechanically** (corrected from 「Two」 by at-tier contract review — see
below). The retired arm held the
last `.transform()` in the `system/MetricsConfig` and
`system/TracingConfig` subtrees, so both defs
now project in output mode instead of falling back to the input shape.
Consequences, all declared
in-diff: `system/MetricsConfig:slis` and `system/TracingConfig:sampling`
publish the `default` the
parser has always applied (declared in `DEFAULT_CHANGES_BY_MAJOR` with
the `ai/KnowledgeSource:refresh`
row as the precedent, that mechanism run backwards), and the nested type
cells of both reference
pages lose the `?` from their default-bearing keys — the output-mode
signature, and the same
convention every transform-free def in the repo already publishes under.
**No runtime default
moves**: measured by byte-identity of the untouched `.default(…)` and by
parsing a minimal config
  on the built package.
- **Consumers swept beyond the named file surface, because they break
otherwise.** The
`evaluated-slot-population.test.ts` census drops 36 positions over 34
declaring lines to 34 over 32,
naming both departures rather than subtracting them; the
`evaluated-slot-union.ts` docblock drops
five of 36 to three of 34; the ADR-0058 D7 ledger row
`cel-declared-unwired-observability` closes
with the removal, and its companion test's `inline` scan floor drops 3
to 1 with both positions
  named, exactly as that floor's own instruction requires.
- **The ruling's Execution section says `Clause-②: no`; the dispatch
claim comment says
`Clause-②: yes`.** This PR carries the claim's line, because the claim
comment is the carrier the
clause-② check reads and the two must agree. Flagged rather than
silently chosen. The `yes` reading
also has independent support in this diff: two published JSON Schemas
change projection direction.
- **Noted, not filed:** the reference pages' inline nested type cells
render post-parse optionality
(a defaulted key reads as required) while the expanded `Nested Shape:`
sections below them read the
zod node and say `optional (default: …)`. The two disagree for every
output-mode def in the repo,
not only these; it predates this card and this diff does not widen it.
Carrier for anyone who picks
  it up: `packages/spec/scripts/build-docs.ts`.
- **Not in this diff, by the ruling:** the structured arms (their own
card);
`skills/objectstack-formula/SKILL.md`, whose `structured | cel` row for
`metrics` / `tracing` is the
skills lane's at tier; and `packages/spec/src/shared/expression.zod.ts`.

## Verification

Run under the shared verify lock; the judged line of each is quoted in
the report on the card.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2)_

- **Same-major absorption (added after at-tier contract review found it
missing — the round's one BLOCKING finding).**
The same unpublished step 18 carried
`entries/semantic/18.evaluated-expression-slots-source-required.ts`,
which still enumerated these two slots among 「the 36 declaring
positions」 and still told an upgrader to
give a sampling `condition` a dialect and a non-blank `source` — **the
exact envelope this head refuses**.
The playbook's same-major rule applies to the published D3 record
exactly as it applied to the census test
and the helper docblock. That entry now reads 34 positions, drops the
two slots and the `condition`-specific
sweep clause, and routes a hit at either slot to
`observability-cel-predicates-retired`. Verified in the
GENERATED output, not only the input: `registry.ts` carries `34
declaring positions` once and
  `36 declaring positions` **zero** times.

- **Why D3 is right rather than merely available.** Both error-map
precedents this retirement copies its
MECHANISM from also registered a D2 conversion, because for them a
mechanical rewrite existed. Here none
does: a strip leaves a REQUIRED `successCriteria` missing (the SLI stops
parsing) and a composite branch
  with no condition at all.

- **The four defs, named** (the two nested ones were disclosed nowhere
before): `system/MetricsConfig`
(`default` on `slis`, plus 8 `required` members) ·
`system/TracingConfig` (`default` on `sampling`, plus 4)
· `system/ServiceLevelIndicator` (one `required` member, `enabled`) ·
`system/TraceSamplingConfig`
(one `required` member, `rules`). ⭐ The last two are invisible to the
`default-changes.ts` table **by the ratchet's construction**, not for
lack of a
default: that table records default VALUES per key, and `enabled` /
`rules` already carried theirs (`true`, `[]`)
published at the base and unmoved here, so no row of it can express a
`required` growth. ⛔ Corrected from an
earlier wording of mine that said they had 「no default to declare」 —
at-tier contract review measured that as
loose; the exact form is the changeset's own: only the first two carry a
`default` MOVE.

---
_Body edits above made by the `domain:spec#4` seat after at-tier
contract review; the dev writes the body once, at creation._

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… is a credential (objectstack-ai#18335) (objectstack-ai#19322)

Fixes objectstack-ai#18335

Clause-②: no

⛔ **Governed surface — this PR parks as a draft by design.**
`docs/adr/**` is Tier H (Prime Directive objectstack-ai#14). No ready-flip, no
enqueue, no auto-merge, and no approval by any agent seat. An authorized
human approval is owed before this lands; a draft with the work done is
the complete deliverable.

## What this lands

ADR-0090 D10 rule 4 read 「every write records `performed_by` (agent) +
`on_behalf_of` (user) + run id」. No door has ever read it that widely,
and the gap had never been written down. Per the ruling on the card
(comment 5690859150, batch objectstack-ai#139 item 1, letter **A**, maintainer 「同意」
2026-09-16, reaffirmed at 5731818788), this PR closes it documentarily:

1. **Rule 4 is narrowed** to *every write by an agent principal*, and it
now says what an agent principal IS — a caller a door resolves to
`principalKind: 'agent'`, today the OAuth / MCP client door alone — so a
later reader can classify a new caller type without re-litigating. It
also states the positive reading of the absence: a missing
`performed_by` is the record that the principal acted for itself.
2. **A dated note** at the end of D10 (`Note (2026-09-16, objectstack-ai#18335)`)
records that an API key is its owner's **credential**, not an agent
principal, and *why* — a credential is how a principal acted, never a
second who — plus what was refused (API keys as a principal category of
their own) and on what basis.

Documentary only. **No code**: one file, +47 / -2.

## The ruling's premise, measured rather than inherited

The ruling asserts that API-key writes audit as the owner today. A note
that misdescribed the enforcement would recreate the very defect this
card closes, so the assertion was measured first. All readings against
`origin/main` at base `e3b3cdd`, taken 2026-09-20T11:05–11:15Z.

| reading | result |
|---|---|
| the one seam that produces an agent principal |
`packages/core/src/security/assemble-execution-context.ts#entryFields` —
`const agent = !anonymous && oauth?.clientId ? oauth : undefined`,
consumed by `principalKind`, `onBehalfOf` and `performedBy` |
| `principalKind` / `onBehalfOf` / `performedBy` in
`packages/core/src/security/api-key.ts` | **0 / 0 / 0** — lit control:
`userId` reads **6** in the same file |
| the same three in
`packages/core/src/security/resolve-authz-context.ts` | **0 / 0 / 0** —
lit control: `userId` reads **48** in the same file |
| the same three in `packages/runtime/src/security/api-key.ts` | **0 / 0
/ 0** (a 25-line re-export module) |
| which doors honour an API key, and what each hands the assembler |
**all three** that run `resolve-authz-context.ts#resolveAuthzContext`.
REST and MCP **stdio** pass `oauth: undefined` **by construction**
(`rest-server.ts`, `mcp/src/plugin.ts#resolveStdioExecutionContext`);
the runtime / MCP **HTTP** dispatcher excludes keys with a **guard**,
`resolve-execution-context.ts#extractJwtBearer`, refusing an
`osk_`-prefixed or non-JWT bearer |
| repo-wide non-test writers of `performedBy` | the MCP/OAuth seam, the
spec + hook declarations of the field, and the audit writer that reads
it. No API-key path |

⇒ an API-key caller falls through `agent ? 'agent' : anonymous ? 'guest'
: 'human'` to `human`, carries no `onBehalfOf` and no `performedBy`, and
its `sys_audit_log` row is the owner's own. **The measurement agrees
with the ruling's premise**, so the narrowing describes the enforcement
rather than changing it.

## Was there a dangling D6 sentence?

No. The clause 「explain (D6) reports both sides of the intersection」
lives *inside* rule 4, so the narrowing carries it. Measured on the ADR:
`attribution` occurs once in the whole file (rule 4) and `both sides`
once (its second line). The companion `docs/design/permission-model.md`
does not restate the rule at all — `performed_by` / `performedBy` /
`attribution` / `every write` read **0** there, against a lit control of
**14** for `agent`. The generated `content/docs/references/**` tables
carry the field's own `.describe()` text, which already says 「Set only
at the /mcp OAuth door … absent everywhere else」 — already narrow,
nothing to correct.

## Gates

Derived in this worktree from the real change set and reconciled with
the run log:

```
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack     # 18 commands
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RAN_FILE
  Run reconciliation — 18 derived, 18 run, 0 NOT-MEASURED, 0 UNRUN.
  EXIT CODES — all 18 accounted famil(ies) carry one, so the NOT-MEASURED count above is DERIVED from them.
```

All 18 exit 0, each captured to disk **before** any pipe.
`check-adr-links`, `check-adr-symbol-anchors` (+ both self-tests),
`check:adr-anchors`, `check:doc-authoring`, `check:nul-bytes`,
`check:pm-governed-merges`, `check:pm-prior-rulings`,
`check:comment-mask-corpus`, `check:cross-package-test-inputs`,
`check:driver-memory-census`, `check:refd-timer-probe`,
`check:watch-hint-literal`, `check:ci-filter-parity`,
`check:closing-keyword-parity` (+ self-test) are among them.
`check:doc-formula-expressions` first exited **3** — `PREREQUISITE NOT
MET`, the gate's own "nothing was measured" code — and was re-run to
exit 0 after `turbo run build --filter=@objectstack/formula
--filter=@objectstack/lint`, taken under the shared verify lock.

The three new symbol anchors in the note (`#entryFields`,
`#resolveApiKeyAdmission`, `#resolveAuthzContext`) resolve as
`declaration` class; no line-number anchor was introduced.

## 维护者速读(草稿)

**改了什么。** 只动一份决策记录(ADR-0090),一个文件,47 行增、2
行删。一句原本写着「**每一次**写入都要记下『谁代谁干的』」的规则,被收窄成「**代理主体**的每一次写入」;同一节末尾新增一段带日期的说明,写明
**API key 不在这条规则里,以及为什么**。⛔ 没有改任何代码,平台行为一行都没变。

**为什么改。** 平台里只有一个地方会把调用方判成「AI 代理」:MCP 的 OAuth 门。**三个门都认 API
key**,但没有一个会把它变成代理:REST 与 MCP stdio 结构上就不传 OAuth 凭据,MCP HTTP
门则靠一道两行的**守卫**挡住 `osk_` 开头的
bearer。三条路都只认出**钥匙的主人**,所以审计日志记的就是主人本人——这一点本轮在源码上实测过,与裁定的前提一致。于是规则写的是「每一次」,实现做的是「只有代理那一次」,这就是**声明面与执行面对不上**。维护者已裁
A(API key 是主人的**凭证**,不是第二个「谁」),裁定里同时明写:只维持现状而不改那句话,等于新留一条「说的和做的不一致」。这份
PR 就是把那句话改对,并把理由记在案。

**风险与代价(含回滚)。** 风险低:纯文档,无运行时、无 API、无数据结构变化,不发布任何 npm
包。真正的代价是**语义上的**:这条规则从此明确**不**覆盖 API
key。将来若出现合规要求「要分得清是人按的还是钥匙跑的」,那是一张新卡、新决策,而不是重读这一条——这一点也写进了 Note 里。回滚是一次
`git revert`,零迁移、零数据影响。

**席位意见。**

**你要做的(一个动作)。** 读一遍 D10 rule 4 那六行和它后面那段带日期的
Note,回「同意」或指出要改的措辞。这是受管面(`docs/adr/**`,Tier H),在你点头之前它会一直停在 draft。

## Acceptance notes

- **`skip-changeset` is owed and was deliberately NOT applied.** This
diff publishes nothing (`docs/adr/**`), so `Check Changeset` needs that
label; the dispatch forbids this executor from touching any label on a
governed-surface PR, so the label is left to the seat. Until it is
applied, `Check Changeset` is expected red and that red is not a finding
about this diff.
- **Noted, not filed — D10's 2026-07 Status blockquote still lists 「the
agent audit-provenance gap」 as an open follow-up.** That gap is the one
objectstack-ai#17022 covered, and that card is `closed`/`completed`
(2026-09-16T06:41Z). This is a stale pointer in prose, not a
reproducible defect, not a contract violation and not a
metadata-authoring trap, so it is not one of the three filing classes.
Carrier: **objectstack-ai#18374**, which already owns the residual close-out in the
same D10 area.
- **Seat assumption 2 re-measured wider than dispatched.** The dispatch
named four PRs; all **36** open PRs in the repo were read at
2026-09-20T11:10Z (`GET /pulls/{n}/files`). Exactly one touches
`docs/adr/**` — objectstack-ai#18985, on ADR-0089 and ADR-0137, disjoint files. Zero
open PRs hold ADR-0090. One caveat recorded rather than hidden: PR
objectstack-ai#17076 (`chore: version packages`) has more than 200 files; pages 1 and
2 were read (200 files, zero `docs/adr/` hits) and the tail was not
enumerated.
- **A small line drift in the dispatch's measurement table, reported as
instructed.** The card body cites the agent channel at `:293` consumed
at `:316`/`:317`; on `e3b3cdd` those are `:294`, `:317`, `:318` — the
file gained the `performedBy` line from objectstack-ai#18371. The rule 4 sentence
itself was anchored on its text and matched verbatim, at line 391 as the
seat read it.

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

3 participants